﻿using System;
using System.Net.Sockets;
using System.Net;
using System.Diagnostics;
using System.Text;

namespace HOPETechnik
{    
    //client must implement this interface to get socket events
    public interface ISocketEventsCallback
    {
        void OnSocketAccepted(CAsyncSocket acceptedSocket);
        void OnSocketConnected(CAsyncSocket sock);
        void OnSocketDisconnected(CAsyncSocket sock);
        void OnSocketDataReceived(CAsyncSocket sock);
        void OnSocketDataSent(CAsyncSocket sock);        
        void ReportError(CAsyncSocket sock, System.Net.Sockets.SocketException ex);
    };

    public enum sockType
    {
        TCP, UDP
    }

    public class CBuffer
    {
        public int nBufferSize { get; set; }
        int m_nPayloadSize;
        int m_nBytesLeft;
        public int nBytesLeft
        {
            get { return m_nBytesLeft; }
            set { m_nBytesLeft = value; }
        }
        public int nPayloadSize
        {
            get
            { return m_nPayloadSize; }
            set
            {
                m_nPayloadSize = value;
                m_nBytesLeft = value;
            }
        }
        public byte[] buffer;
        
        
        public CBuffer(int nBufSize)
        {
            nBufferSize = nBufSize;
            m_nBytesLeft = nBufSize;
            m_nPayloadSize = 0;            
            buffer = new byte[nBufSize];
        }

        public CBuffer(CBuffer buf)
        {
            nBufferSize = buf.nBufferSize;
            m_nBytesLeft = nBufferSize;
            nPayloadSize = 0;
            buffer = new byte[nBufferSize];
        }

        public void Reset()
        {
            nBytesLeft = nBufferSize;
            nPayloadSize = 0;
            Array.Clear(buffer, 0, nBufferSize);
        }
    }
        
    public class CAsyncSocket:IDisposable
    {
        public static readonly string strEndingDelimiter = "<EOF/>";
        const int MAX_BUFFERSIZE= 4*1024*1024;
        sockType m_type;
        CBuffer m_rcvBuffer;
        CBuffer m_sendBuffer;
        Socket m_socket;
        ISocketEventsCallback m_Callback=null;
        bool m_bDisconnectInitiated=false;
        bool m_bDisposed = false;
        string m_strLocalInfo;
        string m_strPeerInfo;

        public CBuffer rcvBuffer
        {
            get { return m_rcvBuffer; }
        }

        public CBuffer sendBuffer
        {
            get { return m_sendBuffer; }
        }

        public string PeerInfo
        {
            get { return m_strPeerInfo; }
            set { m_strPeerInfo = value; }
        }
        
        public string LocalInfo
        { 
            get { return m_strLocalInfo; }
            set { m_strLocalInfo = value; }
        }

        public CAsyncSocket(ISocketEventsCallback callback, int nBufSize=MAX_BUFFERSIZE, sockType type=sockType.TCP)
        {
            m_type = type;
            m_rcvBuffer = new CBuffer(nBufSize);
            m_sendBuffer = new CBuffer(nBufSize);
            m_Callback = callback;
        }

        public CAsyncSocket(CAsyncSocket sock)
        {            
            m_rcvBuffer = new CBuffer(sock.m_rcvBuffer);
            m_sendBuffer = new CBuffer(sock.m_sendBuffer);
            m_type = sock.m_type;            
            m_Callback = sock.m_Callback;
        }

        public void SendData()
        {
            try
            {                
                m_socket.BeginSend(sendBuffer.buffer, 0, sendBuffer.nPayloadSize, 0,
                    new AsyncCallback(SocketSendCallback), this);
            }
            catch (SocketException ex)
            {
                if (m_Callback != null)
                    m_Callback.ReportError(this, ex);
            }
        }
        
        
        public void Connect(string strDestIP, int nDestPort, string strSrcIP="0.0.0.0", int nSrcPort=0)
        {
            try
            {
                IPAddress src = IPAddress.Parse(strSrcIP);
                IPAddress dest = IPAddress.Parse(strDestIP);
                IPEndPoint remote = new IPEndPoint(dest, nDestPort);
                IPEndPoint Local = new IPEndPoint(src, nSrcPort);
                m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                m_socket.Bind(Local);//use specified local IP and port to connect
                
                // Connect to the remote endpoint.
                m_socket.BeginConnect(remote, new AsyncCallback(SocketConnectedCallback), this);
            }
            catch (SocketException ex)
            {
                if (m_Callback != null)
                    m_Callback.ReportError(this, ex);
            }
        }

        public bool StartListening(int nListeningPort, string strSrcIP="0.0.0.0", int backlog=10)
        {
            bool bSuccess = false;
            try
            {
                IPAddress src = IPAddress.Parse(strSrcIP);
                IPEndPoint Local = new IPEndPoint(src, nListeningPort);
                m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);                
                m_socket.Bind(Local);//use specified local IP and port to connect
                m_socket.Listen(backlog);

                //trigger an async accept to wait for incoming connections
                m_socket.BeginAccept(new AsyncCallback(SocketAcceptedCallback), this);
                bSuccess = true;
                m_strLocalInfo = m_socket.LocalEndPoint.ToString();
            }
            catch (SocketException ex)
            {
                if (m_Callback != null)
                    m_Callback.ReportError(this, ex);
            }
            return bSuccess;
        }
        

        public void Disconnect()
        {
            try
            {
                m_bDisconnectInitiated = true;
                m_socket.Shutdown(SocketShutdown.Both);                
                m_socket.Close();
            }
            catch (SocketException ex)
            {
                if (m_Callback != null)
                    m_Callback.ReportError(this, ex);
            }
        }

        static void SocketConnectedCallback(IAsyncResult ar)
        {
            CAsyncSocket me = (CAsyncSocket)ar.AsyncState;
            try
            {
                me.m_socket.EndConnect(ar);
                me.LocalInfo = me.m_socket.LocalEndPoint.ToString();
                me.PeerInfo = me.m_socket.RemoteEndPoint.ToString();
                if (me.m_Callback != null)
                    me.m_Callback.OnSocketConnected(me);

                //now that it's connected, trigger an async read
                me.m_socket.BeginReceive(me.m_rcvBuffer.buffer, 0,
                    me.m_rcvBuffer.nBufferSize, 0, new AsyncCallback(SocketReadCallback), me);
            }
            catch (SocketException ex)
            {
                if (me.m_Callback != null)
                    me.m_Callback.ReportError(me, ex);
            }
        }

        static void SocketAcceptedCallback(IAsyncResult ar)
        {
            CAsyncSocket me = (CAsyncSocket)ar.AsyncState;
            if (me.m_bDisposed)
                return;
            CAsyncSocket clone = new CAsyncSocket(me);
            clone.m_socket = me.m_socket.EndAccept(ar);
            clone.PeerInfo = clone.m_socket.RemoteEndPoint.ToString();
            clone.LocalInfo = clone.m_socket.LocalEndPoint.ToString();

            if (me.m_Callback != null)
                me.m_Callback.OnSocketAccepted(clone);

            //trigger a read on the newly accepted socket to respond to incoming data
            clone.m_socket.BeginReceive(clone.m_rcvBuffer.buffer, 0,
                clone.m_rcvBuffer.nBufferSize, 0, new AsyncCallback(SocketReadCallback), clone);

            //trigger a new async accept on the server port to wait for incoming connection request            
            me.m_socket.BeginAccept(new AsyncCallback(SocketAcceptedCallback), me);
        }

        static void SocketReadCallback(IAsyncResult ar)
        {
            CAsyncSocket me = (CAsyncSocket)ar.AsyncState;

            if (me.m_bDisconnectInitiated)
               return; //initiated disconnection, don't process read as socket is disposed.
            try
            {                
                int bytesRead = me.m_socket.EndReceive(ar);                
                if (me.m_Callback != null)
                {
                    if (bytesRead == 0)//remote disconnected
                        me.m_Callback.OnSocketDisconnected(me);
                    else
                    {                        
                        me.rcvBuffer.nPayloadSize = bytesRead;  
                        me.m_Callback.OnSocketDataReceived(me);
                        
                        //always reset read buffer after callback
                        me.rcvBuffer.Reset();                                                         

                        //trigger another async read                        
                        me.m_socket.BeginReceive(me.m_rcvBuffer.buffer, 0,
                            me.m_rcvBuffer.nBufferSize, 0, new AsyncCallback(SocketReadCallback), me);
                    }
                }
            }
            catch (SocketException ex)
            {
                if (me.m_Callback != null)
                {
                    //if (ex.ErrorCode == 10054)
                      //  me.m_Callback.OnSocketDisconnected(me);                    
                    me.m_Callback.ReportError(me, ex);                
                }
            }                                    
        }

        static void SocketSendCallback(IAsyncResult ar)
        {
            CAsyncSocket me = (CAsyncSocket)ar.AsyncState;
            try
            {
                me.sendBuffer.nBytesLeft -= me.m_socket.EndSend(ar);
                if (me.sendBuffer.nBytesLeft == 0)
                {
                    //data sent, callback to notify ready to send
                    me.sendBuffer.Reset();
                    if (me.m_Callback != null)
                        me.m_Callback.OnSocketDataSent(me);                    
                }
                else
                {
                    int nOffset = me.sendBuffer.nPayloadSize - me.sendBuffer.nBytesLeft;
                    //keep sending till done
                    me.m_socket.BeginSend(me.sendBuffer.buffer, nOffset,
                        me.sendBuffer.nBytesLeft, 0, new AsyncCallback(SocketSendCallback), me);
                }
            }
            catch (SocketException ex)
            {
                if (me.m_Callback != null)
                    me.m_Callback.ReportError(me, ex);
                me.sendBuffer.Reset();
            }
        }

        public void Dispose()
        {
            m_bDisposed = true;
            m_socket.Dispose();            
        }
        
    };
}
